Kealdish's Studio.

iOS 10 UICollectionView新特性小记

字数统计: 2.4k阅读时长: 8 min
2016/07/18 Share

预览

iOS 10对于UICollectionview的改进优化主要表现在以下三个方面:

  • 平滑的滑动体验
  • self-sizing改进
  • interactive reordering

平滑的滑动体验

1.卡顿的原因

在iOS 10之前我们在使用UICollectionview大幅度滑动加载复杂的视图时,很可能会出现卡顿掉帧的情况,这是很影响用户体验的。那为什么会出现这种情况呢?从下面的图可以看出,当cell准备显示到屏幕上时,cell的加载工作就已经完成了。但注意,在屏幕外加载的数据是一整行的cell。这样大量的加载工作在快速滑动的过程中便会稍显吃力,也就造成了卡顿和掉帧。

下图中绘制出了上图的实验数据。其中,x轴表示显示刷新事件,y轴表示主线程中CPU处理的时间。我们知道手机屏幕的刷新率在60FPS,也就意味着每16ms就要刷新一次,若CPU处理的时间超过16ms,便会将加载挤压到下一帧从而引起卡顿。从图中可以看到当屏幕快速滑动时,CPU处理时间超过16ms,会有卡顿,而在缓慢滑动过程中,CPU的处理时间很短,不会有卡顿。

那如何解决这个问题呢?我们需要将Cell的加载工作分摊,达到下图的效果,就可以解决这个问题了。那如何分摊cell的加载工作呢?这就要涉及到Cell的生命周期了。

2.Cell Life Cycle

在iOS 10之前,当我们滑动UICollectionView时,就需要添加新的cell。我们将它从reuse队列中拿出来,然后调用prepareForReuse。这个方法让cell将其本身重置为默认状态,接收并加载app中的新数据。

接下来,我们会继续调用cellForItemAtIndexPath方法。这里主要是你做填充cell工作的地方,你可以加载数据模型,并将其设置到cell中,然后返回给系统。

之后,在cell显示到屏幕之前,我们会调用willDisplayCell方法。这个方法是让你做cell显示到屏幕上最后工作的地方。

当Cell从屏幕上滑出时,我们会调用didEndDisplayingCell方法。这就是iOS 10之前整个cell的生命周期。

在iOS 10中,当我们滑动UICollectionView需要添加新的cell时,我们会从reuse队列中拿出来,然后,调用prepareForReuse方法。

接下来,在cellForItemAtIndexPath方法中填充cell的内容。

用户继续滑动,接下来发生的事情有所不同。我们不再在创建cell的时候就调用willDisplayCell方法,该方法的调用时机放到cell显示到屏幕上的时候。

接着,用户继续滑动。当cell离开UICollectionview的可视区域时,我们会调用didEndDisplayingCell方法。在iOS 10之前,在此时cell就进入了reuse队列中。当这个cell需要重新显示数据的时候,我们就必须要重新进行一遍cell的生命周期。但是在iOS 10中,我们会将cell保持一段时间。在这段时间内,用户想查看已经滑出屏幕的cell往回滑动时,cell不需要再重新走一遍生命周期,只需要调用willDisplayCell即可。

在一行多列的情况中同样适用。我们一次加载一个cell而不是一行cell来获得更好的滑动体验。我们先加载第一个cell,加载完再加载第二个cell,当两个cell加载完成后并且cell即将显示到屏幕上时,我们调用两者的willDisplayCell方法。

下图是模拟iOS 10下系统加载cell的情况。可以看到iOS 10下的滑动相比iOS 9流畅很多,没有卡顿。

3.Cell Pre-Fetching

iOS 10引入了Pre-Fetching,默认情况下是开启的。若你必须用到iOS 10之前老的生命周期,只需要在UICollectionview中添加isPrefetchingEnabled属性。

1
collectionView.isPrefetchingEnabled = false

为了能最好地发挥这个特性,我们要做到在cellForItemAtIndexPath方法中做耗费资源和时间的工作,包括cell的所有内容的创建。另外,我们要确保在willDisplayCelldidEndDisplayCell方法中做轻量甚至不做工作。

更进一步,我们该如何处理繁重的data models?在cell内容创建的过程中需要处理繁重的任务,比如图片解码、连接数据库、从CoreData中加载数据。为了解决这个问题,iOS 10引入了新的API帮助告诉你的数据模型如何加载内容。

UICollectionView有两个组成部分:dataSource和delegate。iOS 10引入新的组成元素:prefetchDataSource。它是可选的,并且只必须实现一个方法。

1
2
3
4
5
6
7
8
protocol UICollectionViewDataSourcePrefetching {
func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [NSIndexPath])
optional func collectionView(_ collectionView: UICollectionView, cancelPrefetchingForItemsAt indexPaths: [NSIndexPath])
}
class UICollectionView : UIScrollView {
weak var prefetchDataSource: UICollectionViewDataSourcePrefetching?
var isPrefetchingEnabled: Bool
}

第一个方法叫prefetchItemsAt indexPaths。该方法会在你异步预加载模型中的内容时在prefetch data source中调用。方法中的indexpath参数是有序数组可以帮助更好的读取模型中的数据。第二个方法叫cancelPrefetchingForItemsAt indexPaths(可选)。当我们决定不再滑动到某个indexPath集合时会被prefetch data source调用。你可以调用它来取消或者降低任何预加载的优先级。

需要注意的是,这不是对数据模型的代替。它只是对已经存在的异步加载数据方法的起到辅助作用。

下面总结一下使用预加载的注意点:

1.在调用方法时,确保所处理的工作都是在后台线程中进行。配合GCD和NSOperationQueue去处理多线程任务。
2.记住pre-fetch是自适应技术。当滑动缓慢时,pre-fetch会做额外的工作去优化。当滑动快速时,需要频繁刷新屏幕,这时候就不再做pre-fetch。
3.调用cancelPrefetchingForItemsAt indexPaths方法去处理用户滑动操作的切换。

在iOS 10中,与UICollectionview一脉相承的UItableview也加入了相同的API。

1
2
3
4
5
6
7
protocol UITableViewDataSourcePrefetching {
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths:[NSIndexPath])
optional func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths:[NSIndexPath])
}
class UITableView : UIScrollView {
weak var prefetchDataSource: UITableViewDataSourcePrefetching?
}

self-sizing cell

UICollectionView中有一个实体布局类UICollectionViewFlowLayout,它已经全面支持self-sizing cells。要开启这一特性,需要设置estimated item size为不为0的CGSize。它会告诉UICollectionView你想要在内容显示的时候动态计算布局。

1
layout.estimatedItemSize = CGSize(width:50,height:50)

为了能够准确的得到cell的真实尺寸,有三种方法可以达到需求。

1.AutoLayout。当给cell的contentView的所有层级的视图添加约束后,自动布局系统自动计算出cell的大小。
2.重写sizeThatFits()方法。如果你不想使用自动布局,并想手动控制就用这个方法。
3.重写preferredLayoutAttributesFittingAttributes()方法。该方法不仅提供尺寸信息还能提供像变换、alpha等属性信息。

但是在实际操作中,我们很难去设置合适的estimated item size。如果flowlayout可以依据数学计算出的尺寸而不是根据布局内容的真实尺寸去适应那会非常棒。为此,iOS 10引入了新的API。

1
layout.estimatedItemSize = UICollectionViewFlowLayoutAutomaticSize

我们需要做的就是将estimated item size设置为UICollectionViewFlowLayoutAutomaticSize,flowlayout会自动计算布局,它不仅能计算所有cell当前的size,还能依此动态预测接下来的cell的size大小。

接下来看两个例子更能明显看出差别。

在下图中可以看到在iOS 10之前,当单个cell的尺寸改变时,其他的cell没有变化,布局需要重新计算。

在iOS 10中,当第一个cell的size发生改变时,系统会自动计算出所有cell的size大小,然后刷新界面。

Interactive Reordering

先看看iOS 9里面的API:

1
2
3
4
5
6
class UICollectionView : UIScrollView {
func beginInteractiveMovementForItem(at indexPath: NSIndexPath) -> Bool
func updateInteractiveMovementTargetPosition(_ targetPosition: CGPoint)
func endInteractiveMovement()
func cancelInteractiveMovement()
}

要想开启interactive movement,我们就需要调用beginInteractiveMovementForItem()方法,其中indexPath代表了我们将要移动走的cell。接着每次手势的刷新,我们都需要刷新cell的位置,去响应我们手指的移动操作。这时我们就需要调用updateInteractiveMovementTargetPosition()方法。我们通过手势来传递坐标的变化。当我们移动结束之后,就会调用endInteractiveMovement()方法。 UICollectionView就会放下cell,处理完整个layout,此时你也可以重新刷新model或者处理数据model。如果中间突然手势取消了,那么这个时候就应该调用cancelInteractiveMovement()方法。如果我们重新把cell移动一圈之后又放回原位,其实就是取消了移动,那这个时候就应该在cancelInteractiveMovement()方法里面不用去刷新data source。

在iOS 10中,如果你使用UICollectionViewController,那么这个重排对于你来说会更加的简单。

1
2
3
class UICollectionViewController : UIViewController {
var installsStandardGestureForInteractiveMovement: Bool
}

你只需要把installsStandardGestureForInteractiveMovement这个属性设置为True即可。CollectionViewController会自动为你加入手势,并且自动为你调用上面的方法。

iOS 10在iOS 9的基础上增加了翻页功能。

1
collectionView.isPagingEnabled = true

开启分页前:

开启分页后:

UIRefreshControl

UIRefreshControl现在可以直接在CollectionView里面使用,同样的,也可以直接在UITableView里面使用,并且可以脱离UITableViewController。因为现在RefreshControl成为了ScrollView的一个属性了。

1
2
3
4
let refreshControl = UIRefreshControl()
refreshControl.addTarget(self, action: #selector(refreshControlDidFire(_:)),
for: .valueChanged)
collectionView.refreshControl = refreshControl

UIRefreshControl的使用方法很简单,先创建一个refreshControl,再关联一个action事件,最后把这个新的refreshControl赋给想要的控件的对应的属性即可。

CATALOG
  1. 1. 预览
  2. 2. 平滑的滑动体验
    1. 2.0.1. 1.卡顿的原因
    2. 2.0.2. 2.Cell Life Cycle
    3. 2.0.3. 3.Cell Pre-Fetching
  • 3. self-sizing cell
  • 4. Interactive Reordering
  • 5. UIRefreshControl